package ru.batrdmi.svnplugin;

import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxStylesheet;
import ru.batrdmi.svnplugin.logic.FileRevisionHistory;
import ru.batrdmi.svnplugin.logic.Revision;

import javax.swing.*;
import java.awt.*;
import java.util.*;
import java.util.List;

public class RevisionGraphModel extends mxGraph {
    private List<Revision> revisions;
    private Revision currentRevision;
    private LinkedHashMap<String, ArrayList<Revision>> revMap;
    private HashMap<Revision, mxCell> cellMap;
    private HashMap<Revision, Integer> revisionOrder;
    private HashMap<Revision, HashSet<Revision>> linkedRevisions;

    public RevisionGraphModel(FileRevisionHistory revisionHistory, boolean collapseRevisions, boolean onlyImpactingRevisions) {
        setCellsMovable(false);
        setCellsEditable(false);
        setCellsResizable(false);
        setDropEnabled(false);

        if (onlyImpactingRevisions) {
            revisionHistory = retainOnlyImpacting(revisionHistory);
        }
        processHistory(revisionHistory);

        getModel().beginUpdate();
        createGraph(collapseRevisions);
        layoutGraph();
        getModel().endUpdate();
    }

    private FileRevisionHistory retainOnlyImpacting(FileRevisionHistory history) {
        class RevInfo {
            final Revision thisRev, prevRev;

            RevInfo(Revision thisRev, Revision prevRev) {
                this.thisRev = thisRev;
                this.prevRev = prevRev;
            }
        }
        // preprocess data
        List<Revision> sortedRevisions = new ArrayList<Revision>(history.getAllRevisions());
        Collections.sort(sortedRevisions, new Revision.RevisionNumberComparator());
        Map<String, Revision> pathHeads = new HashMap<String, Revision>();
        Map<Revision, RevInfo> preprocessedData = new HashMap<Revision, RevInfo>();
        for (Revision r : sortedRevisions) {
            preprocessedData.put(r, new RevInfo(r, pathHeads.get(r.getRelPath())));
            pathHeads.put(r.getRelPath(), r.isDeleted() ? null : r);
        }
        
        // walk the revision graph and collect impacting revisions
        Set<Revision> result = new HashSet<Revision>();
        Queue<Revision> revsToProcess = new LinkedList<Revision>();
        revsToProcess.add(history.getCurrentRevision());
        while (!revsToProcess.isEmpty()) {
            RevInfo ri = preprocessedData.get(revsToProcess.remove());
            if (ri != null && !result.contains(ri.thisRev)) {
                result.add(ri.thisRev);
                if (!ri.thisRev.getCopiedOrMergedRevisions().isEmpty()) {
                    revsToProcess.add(ri.thisRev.getCopiedOrMergedRevisions().get(0));
                }
                if (ri.prevRev != null) {
                    revsToProcess.add(ri.prevRev);
                }
            }
        }

        return new FileRevisionHistory(result, history.getCurrentRevision(),
                history.getRepoRoot(), history.getRelPath(), history.isDirectory(), history.getStatus());
    }

    private void processHistory(FileRevisionHistory revisionHistory) {
        revMap = new LinkedHashMap<String, ArrayList<Revision>>();
        currentRevision = revisionHistory.getCurrentRevision();
        revisions = new ArrayList<Revision>(revisionHistory.getAllRevisions());
        revisionOrder = new HashMap<Revision, Integer>();
        linkedRevisions = new HashMap<Revision, HashSet<Revision>>();
        // Sort revisions based on revision dates
        Collections.sort(revisions, new Revision.RevisionNumberComparator());
        
        int i = 0;
        for (Revision rev : revisions) {
            revisionOrder.put(rev, i++);
            
            String path = rev.getRelPath();
            ArrayList<Revision> revsOnPath = revMap.get(path);
            if (revsOnPath == null) {
                revsOnPath = new ArrayList<Revision>();
                revMap.put(path, revsOnPath);
            }
            revsOnPath.add(rev);
            
            // find linked revisions
            for (Revision lr : revisions) {
                if (rev.getRelPath().equals(lr.getLinkedRelPath()) && rev.getRevisionNumber() == lr.getLinkedRevisionNumber()
                        || lr.getRelPath().equals(rev.getLinkedRelPath()) && lr.getRevisionNumber() == rev.getLinkedRevisionNumber()) {
                    HashSet<Revision> s = linkedRevisions.get(rev);
                    if (s == null) {
                        s = new HashSet<Revision>();
                        linkedRevisions.put(rev, s);
                    }
                    s.add(lr);
                }
            }
        }
    }

    private void createGraph(boolean collapseRevisions) {
        cellMap = new HashMap<Revision, mxCell>();
        for (String path : revMap.keySet()) {
            mxCell lastCell = null;
            ArrayList<Revision> revsOnPath = revMap.get(path);
            for (Revision rev : revsOnPath) {
                lastCell = createCell(lastCell, rev, collapseRevisions);
            }
        }
        // create merge/copy links
        for (mxCell cell : cellMap.values()) {
            List<Revision> revs = ((Node) cell.getValue()).revisions;
            if (revs.size() == 1) {
                mxCell mergeFromCell = getLinkedCell(revs.get(0));
                if (mergeFromCell != null) {
                    createEdge(mergeFromCell, cell, true);
                }
            }
        }
        // move all vertices to foreground
        orderCells(false, new HashSet<mxCell>(cellMap.values()).toArray());
    }

    private mxCell createCell(mxCell neighborCell, Revision rev, boolean collapseRevisions) {
        mxCell result;
        List<Revision> prevList = (neighborCell == null) ? null : ((Node)neighborCell.getValue()).revisions;
        Revision prevRev = (prevList == null) ? null : prevList.get(prevList.size() - 1);
        int thisOrder = revisionOrder.get(rev);
        int prevOrder = (prevRev == null) ? -100 : revisionOrder.get(prevRev);
        if (collapseRevisions && prevRev != currentRevision && rev != currentRevision
                && prevRev != null && !linkedRevisions.containsKey(prevRev) && !prevRev.isDeleted()
                && !linkedRevisions.containsKey(rev) && !rev.isDeleted() && (thisOrder - prevOrder) == 1
                && (prevOrder == 0 || revisions.get(prevOrder - 1).getRevisionNumber() < prevRev.getRevisionNumber())
                && (thisOrder == (revisions.size() - 1) || revisions.get(thisOrder + 1).getRevisionNumber() > rev.getRevisionNumber())) {
            prevList.add(rev);
            result = neighborCell;
        } else {
            List<Revision> nodeRevisions = new ArrayList<Revision>();
            nodeRevisions.add(rev);
            result = new mxCell(new Node(nodeRevisions), new mxGeometry(), null);
            result.setVertex(true);
            result.setConnectable(true);
            result.setStyle(SVNRevisionGraph.REVISION_STYLE + ";"
                    + mxConstants.STYLE_STROKEWIDTH + "=" + ((rev == currentRevision) ? "3" : "1")  + ";"
                    + mxConstants.STYLE_DASHED + "=" + rev.isDeleted());
            addCell(result);
            if (prevRev != null && !prevRev.isDeleted()) {
                createEdge(neighborCell, result, false);
            }
        }
        cellMap.put(rev, result);
        return result;
    }

    private mxCell createEdge(mxCell source, mxCell target, boolean arrow) {
        return (mxCell) insertEdge(null, null, new Node(null), source, target,
                arrow ? SVNRevisionGraph.COPY_OR_MERGE_LINK_STYLE : SVNRevisionGraph.IN_BRANCH_LINK_STYLE);
    }

    private void layoutGraph() {
        mxCell lastCell = null;
        Double nextY = null;
        int i = 0;
        for (String path : revMap.keySet()) {
            ArrayList<Revision> revsOnPath = revMap.get(path);
            if (i != 0) {
                Revision firstRev = revsOnPath.get(0);
                lastCell = getLinkedCell(firstRev);
            }
            int relativePosition = SwingConstants.BOTTOM;
            double widestY = 0;
            for (Revision rev : revsOnPath) {
                mxCell cell = cellMap.get(rev);
                if (cell != lastCell) {
                    lastCell = positionCell(lastCell, relativePosition, nextY, cell);
                    relativePosition = SwingConstants.RIGHT;
                    mxRectangle rect = lastCell.getGeometry();
                    widestY = Math.max(widestY, rect.getY() + rect.getHeight());
                }
            }
            nextY = widestY + SVNRevisionGraph.CELL_SPACING;
            i++;
        }
        //Now adjust x position in order of revision numbers
        for (i = 1; i < revisions.size(); i++) {
            Revision rev = revisions.get(i);
            Revision prevRev = revisions.get(i - 1);
            mxCell cell = cellMap.get(rev);
            mxCell prevCell = cellMap.get(prevRev);
            if (cell != prevCell) {
                Rectangle rect = cell.getGeometry().getRectangle();
                positionCell(prevCell,
                        rev.getRevisionNumber() > prevRev.getRevisionNumber() ? SwingConstants.RIGHT : SwingConstants.BOTTOM,
                        rect.getY(), cell);
            }
        }
    }

    private mxCell positionCell(mxCell neighborCell, int relativePosition, Double preferredY, mxCell targetCell) {
        //Calculate position from neighbor
        mxRectangle neighborRect = null;
        if (neighborCell == null) {
            if (relativePosition == SwingConstants.BOTTOM)
                neighborRect = new mxRectangle(SVNRevisionGraph.CELL_SPACING, 0, 0, 0);
            else if (relativePosition == SwingConstants.RIGHT)
                neighborRect = new mxRectangle(0, SVNRevisionGraph.CELL_SPACING, 0, 0);
        } else {
            neighborRect = neighborCell.getGeometry();
        }
        double x = 0, y = 0;
        if (relativePosition == SwingConstants.BOTTOM) {
            x = neighborRect.getX();
            y = neighborRect.getY() + neighborRect.getHeight() + SVNRevisionGraph.CELL_SPACING;
        } else if (relativePosition == SwingConstants.RIGHT) {
            x = neighborRect.getX() + neighborRect.getWidth() + SVNRevisionGraph.CELL_SPACING;
            y = neighborRect.getY();
        }
        if (preferredY != null) {
            y = preferredY;
        }

        mxRectangle preferredSize = getPreferredSizeForCell(targetCell);
        neighborRect = new mxRectangle(x, y, preferredSize.getWidth(), preferredSize.getHeight());
        resizeCell(targetCell, neighborRect);
        return targetCell;
    }

    private mxCell getLinkedCell(Revision rev) {
        if (rev.getLinkedRelPath() == null) {
            return null;
        } else {
            Revision mergeRev = new Revision(rev.getLinkedRelPath(), rev.getLinkedRevisionNumber());
            return cellMap.get(mergeRev);
        }
    }

    public Revision getCurrentRevision() {
        return currentRevision;
    }

    public mxCell getCellForRevision(Revision revision) {
        return cellMap.get(revision);
    }
    
    public Set<String> getAllPaths() {
        return revMap.keySet();
    }
    
    public List<Revision> getRevisionsForPath(String path) {
        return revMap.get(path);
    }

    public Set<Revision> getLinkedRevisions(Revision r) {
        Set<Revision> result = linkedRevisions.get(r);
        return (result == null) ? Collections.<Revision>emptySet() : result;
    }
    
    public int getNodeHeight() {
        if (cellMap.isEmpty()) {
            return 0;
        } else {
            return (int) cellMap.values().iterator().next().getGeometry().getHeight();
        }
    }

    public List<Node> getSelectedNodes() {
        List<Node> nodes = new ArrayList<Node>();
        Object[] cells = getSelectionCells();
        if (cells != null) {
            for (Object cell : cells) {
                mxCell c = (mxCell) cell;
                if (c.isVertex()) {
                    nodes.add((RevisionGraphModel.Node) c.getValue());
                }
            }
        }
        return nodes;
    }
    
    public static class Node {
        public final List<Revision> revisions;
        public final Map<String, Object> properties = new HashMap<String, Object>();

        public Node(List<Revision> revisions) {
            this.revisions = revisions;
        }
    }

    @Override
    public String getLabel(Object o) {
        List<Revision> revisions = ((Node) ((mxCell) o).getValue()).revisions;
        if (revisions == null || revisions.isEmpty()) {
            return null;
        } else if (revisions.size() == 1) {
            return Long.toString(revisions.get(0).getRevisionNumber());
        } else {
            return Long.toString(revisions.get(0).getRevisionNumber()) + "..." 
                    + Long.toString(revisions.get(revisions.size() - 1).getRevisionNumber());
        }
    }

    @Override
    public String getToolTipForCell(Object o) {
        List<Revision> revisions = ((Node) ((mxCell) o).getValue()).revisions;
        if (revisions == null || revisions.isEmpty()) {
            return null;
        } else if (revisions.size() == 1) {
            return revisions.get(0).getMessage();
        } else {
            return revisions.size() + " revisions";
        }
    }

    @Override
    protected mxStylesheet createStylesheet() {
        mxStylesheet s = new mxStylesheet();

        Map<String, Object> style = new HashMap<String, Object>();
        style.put(mxConstants.STYLE_SHAPE, mxConstants.SHAPE_CONNECTOR);
        style.put(mxConstants.STYLE_STROKECOLOR, "black");
        style.put(mxConstants.STYLE_ENDARROW, "no arrow");
        s.putCellStyle(SVNRevisionGraph.IN_BRANCH_LINK_STYLE, new HashMap<String, Object>(style));

        style.put(mxConstants.STYLE_SHAPE, "linkShape");
        style.put(mxConstants.STYLE_ENDARROW, mxConstants.ARROW_CLASSIC);
        s.putCellStyle(SVNRevisionGraph.COPY_OR_MERGE_LINK_STYLE, new HashMap<String, Object>(style));

        style.clear();
        style.put(mxConstants.STYLE_SHAPE, "revisionShape");
        style.put(mxConstants.STYLE_STROKECOLOR, "black");
        style.put(mxConstants.STYLE_FONTCOLOR, "black");
        style.put(mxConstants.STYLE_SPACING, 1);
        style.put(mxConstants.STYLE_SPACING_TOP, 2);
        style.put(mxConstants.STYLE_SPACING_LEFT, 1);
        s.putCellStyle(SVNRevisionGraph.REVISION_STYLE, new HashMap<String, Object>(style));

        return s;
    }
}
